Skip to content

fix(linux): guard host state against concurrent shutdown - #80

Merged
yashranaway merged 1 commit into
mainfrom
t3code/fix-shutdown-race
Aug 5, 2026
Merged

fix(linux): guard host state against concurrent shutdown#80
yashranaway merged 1 commit into
mainfrom
t3code/fix-shutdown-race

Conversation

@yashranaway

Copy link
Copy Markdown
Collaborator

Closes #12 — the one priority:critical item in Phase 1.

The race

shutdown deliberately bypasses the transport's request queue so an operator can always stop a stalled browser action (Transport.swift:213-218). That is a feature, and this PR keeps it. The consequence is that host.stop() runs on the main thread while a normal command is still executing on a client thread.

LinuxBrowserHost held sessions, trace, activeFlows, and recordings as plain dictionaries behind @unchecked Sendable with no lock. Two threads mutating the same Swift dictionary is not a lost update — it is memory corruption. stop() clearing recordings while a record start inserted into it can crash the host process and take every session with it.

The macOS host already serialises the same state through onAgentMain plus a dedicated recordings lock. Linux had nothing. Asymmetric safety for identical state.

The fix

  • All four containers go through a stateLock. The lock is held only around collection access, never across browser I/O, so a stalled command still cannot delay teardown — the exact property the queue bypass exists to provide.
  • stop() is idempotent: flips a stopping flag, snapshots and clears the containers under the lock, then stops recordings, closes sessions, and kills the browser outside it.
  • Session and recording registration re-check stopping under the lock and back out cleanly if teardown has begun. Without that, a recording started during the teardown window leaks an FFmpeg process nothing will ever stop, and a session created in the same window leaks a browser target.

Test

Tests/linux-e2e.sh now drives the real race rather than a proxy: an active recording and a full-page tour in flight, then headless stop, asserting the host exits on its own within 30 s and restarts clean with only the default session. It runs in the Linux E2E (Docker) job.

Verification note

I cannot compile Swift in my environment — no toolchain, and no Docker access — so CI is the first compile of this change. I re-read every edited region and specifically checked for the Swift trap where a helper named session/recording would resolve to a local variable declared later in the same scope; the helpers are named lookupSession/lookupRecording to avoid it. If CI finds something, I will fix it here rather than in a follow-up.

Closes #12.

shutdown deliberately bypasses the transport's request queue so an operator
can always stop a stalled browser action (Transport.swift:213-218). Teardown
therefore runs on the main thread while a normal command is still in flight,
and LinuxBrowserHost held sessions, trace, activeFlows, and recordings as
plain dictionaries behind @unchecked Sendable. Two threads mutating the same
Swift dictionary is memory corruption, not a lost update: host.stop() clearing
recordings while a command inserted one could crash the host and take every
session with it. The macOS host already serialises the same state through
onAgentMain and a recordings lock; Linux had nothing.

All four containers now go through stateLock. The lock is held only around
collection access and never across browser I/O, so a stalled command still
cannot delay teardown — the property the queue bypass exists to provide.

stop() is now idempotent: it flips a stopping flag, snapshots and clears the
containers under the lock, then stops recordings, closes sessions, and kills
the browser outside it.

Session and recording registration re-check stopping under the lock and back
out cleanly if teardown has begun. Without that, a recording started during
teardown would leak an FFmpeg process that nothing would ever stop, and a
session created in the same window would leak a browser target.

Regression coverage in Tests/linux-e2e.sh drives the real race: an active
recording plus a full-page tour in flight, then stop, asserting the host exits
on its own and restarts clean.
@yashranaway
yashranaway requested a review from SarthakWade August 5, 2026 19:47
@yashranaway
yashranaway merged commit 803ac85 into main Aug 5, 2026
7 checks passed
@yashranaway
yashranaway deleted the t3code/fix-shutdown-race branch August 5, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A1 — Shutdown data race on the Linux host

1 participant